package nl.Viewer.Sessions.Communication;

import java.awt.Dimension;
import java.io.InputStream;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Iterator;

import javax.xml.namespace.QName;
import javax.xml.rpc.ParameterMode;
import javax.xml.rpc.ServiceException;

import nl.Viewer.SerializedObjects.Bounds;
import nl.Viewer.SerializedObjects.DataObject;
import nl.Viewer.SerializedObjects.Dataset;
import nl.Viewer.SerializedObjects.Job;
import nl.Viewer.SerializedObjects.LookmarkInfo;
import nl.Viewer.SerializedObjects.Modality;
import nl.Viewer.SerializedObjects.Output;
import nl.Viewer.SerializedObjects.Parameter;
import nl.Viewer.SerializedObjects.ParameterValue;
import nl.Viewer.SerializedObjects.Session;
import nl.Viewer.SerializedObjects.Visualization;

import org.apache.axis.AxisFault;
import org.apache.axis.Message;
import org.apache.axis.attachments.AttachmentPart;
import org.apache.axis.client.Call;
import org.apache.axis.client.Service;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;

/**
 * The main class for client-server communication using the SOAP XML/RPC
 * protocol. A communication object is instantiated with a server-address and an
 * rpc endpoint on that server. A client can call login( .. ) with credentials
 * to have a connection verified with the server. All currently server-exposed
 * functionality is implemented in this class, along with all deserialization
 * logic.
 * 
 * @author Jeroen Verschuur
 * 
 */

public class Communication
{
   public final static Service service       = new Service();
   private String              endpoint;
   private String              serverAddress;
   private String              username      = null;
   private String              password      = null;
   private boolean             isVerified    = false;
   private Exception           lastException = null;
   public String               localClientIdentifier;

   /**
    * server should be <hostname:port> endpoint should for example be
    * /ViewerServer/services/Viewer
    * 
    * @param server
    * @param endpoint
    */
   public Communication( String server, String endpoint )
   {
      this.serverAddress = server;
      this.endpoint = "http://" + server + endpoint;
      try
      {
         localClientIdentifier = InetAddress.getLocalHost().getHostName();
      }
      catch( Exception e )
      {
         localClientIdentifier = "hostname";
         // ignore
      }
      // make it 'unique'
      localClientIdentifier += this.hashCode();
   }

   public Exception getLastException()
   {
      return lastException;
   }

   public boolean isVerified()
   {
      return isVerified;
   }

   public boolean login( String username, String password )
   {
      this.username = username;
      this.password = password;
      try
      {
         // try connection
         getDatasets();
         isVerified = true;
      }
      catch( Exception e )
      {
         isVerified = false;
         lastException = e;
         e.printStackTrace();
      }

      return isVerified;
   }

   public boolean pingServer()
   {
      try
      {
         doSoapCall( "ping", true, 20000 );
         return true;
      }
      catch( Exception se )
      {
         return false;
      }
   }

   public ArrayList<Dataset> getDatasets()
      throws SOAPServerException,
      XMLParseException,
      UserRequestException
   {
      ObjectTuple kv = doSoapCall( "getDatasets" );

      ArrayList<Dataset> datasets = null;
      try
      {
         XStream xstream = new XStream( new DomDriver() );
         xstream.alias( "Dataset", Dataset.class );
         xstream.alias( "Modality", Modality.class );
         xstream.alias( "Visualization", Visualization.class );
         xstream.alias( "Output", Output.class );

         // System.out.println("datasets: ["+(String)kv.obj2+"]");

         datasets = (ArrayList<Dataset>) xstream.fromXML( (String) kv.obj2 );

         return datasets;
      }
      catch( Exception e )
      {
         throw new XMLParseException(
                                      "Method getDatasets failed because of XML parsing error",
                                      e );
      }

   }

   public ArrayList<LookmarkInfo> getLookmarks( int datasetId )
      throws SOAPServerException,
      XMLParseException,
      UserRequestException
   {

      ObjectTuple kv = doSoapCall(
         "getLookmarks",
         new ObjectTuple( "datasetId", datasetId ) );
      ArrayList<LookmarkInfo> lookmarks = null;
      try
      {
         XStream xstream = new XStream( new DomDriver() );
         xstream.alias( "LookmarkInfo", LookmarkInfo.class );

         lookmarks = (ArrayList<LookmarkInfo>) xstream
               .fromXML( (String) kv.obj2 );

         return lookmarks;
      }
      catch( Exception e )
      {
         throw new XMLParseException(
                                      "Method getDatasets failed because of XML parsing error",
                                      e );
      }

   }

   public Long createSession( int modalityId, int visualizationId )
      throws SOAPServerException,
      UserRequestException
   {
      ObjectTuple kv = doSoapCall(
         "createSession",
         true,
         30000,
         new ObjectTuple( "hostname", localClientIdentifier ),
         new ObjectTuple( "modalityId", modalityId ),
         new ObjectTuple( "visualizationId", visualizationId ) );
      if( kv == null )
         return null;

      return (Long) kv.obj2;
   }

   public Long createSession(
         int modalityId,
         int visualizationId,
         int requestedParaViewThreads )
      throws SOAPServerException,
      UserRequestException
   {
      ObjectTuple kv = doSoapCall(
         "createSession",
         true,
         30000,
         new ObjectTuple( "hostname", localClientIdentifier ),
         new ObjectTuple( "modalityId", modalityId ),
         new ObjectTuple( "visualizationId", visualizationId ),
         new ObjectTuple( "requestedThreads", requestedParaViewThreads ) );
      if( kv == null )
         return null;

      return (Long) kv.obj2;
   }

   public Long createSessionFromLookmark( int lookmarkId )
      throws SOAPServerException,
      UserRequestException
   {
      ObjectTuple kv = doSoapCall(
         "createSessionFromLookmark",
         true,
         30000,
         new ObjectTuple( "hostname", localClientIdentifier ),
         new ObjectTuple( "lookmark", lookmarkId ) );
      if( kv == null )
         return null;

      return (Long) kv.obj2;
   }

   public void closeSession( long sessionId )
      throws SOAPServerException,
      UserRequestException
   {
      doSoapCall(
         "closeSession",
         false,
         1000,
         new ObjectTuple( "sessionId", sessionId ),
         new ObjectTuple( "removeJobs", true ) );
   }

   public long submitJob(
         long sessionId,
         String outputName,
         String[] inParameters,
         Object[] inParameterValues )
      throws SOAPServerException,
      UserRequestException
   {
      ObjectTuple kv = doSoapCall(
         "submitJob",
         new ObjectTuple( "sessionId", sessionId ),
         new ObjectTuple( "outputName", outputName ),
         new ObjectTuple( "inParameters", inParameters ),
         new ObjectTuple( "inParameterValues", inParameterValues ) );

      return (Long) kv.obj2;
   }

   public byte[] getDataPart(
         long jobId,
         int resolution,
         int horizontalPart,
         int verticalPart ) throws SOAPServerException, UserRequestException
   {

      ObjectTuple kv = doSoapCall(
         "getDataPart",
         new ObjectTuple( "jobId", jobId ),
         new ObjectTuple( "resolution", resolution ),
         new ObjectTuple( "horizontalPart", horizontalPart ),
         new ObjectTuple( "verticalPart", verticalPart ) );

      return (byte[]) kv.obj2;
   }

   // private static ArrayList<AttachmentPart> parseAttachments( Message
   // responseMessage ) {
   // ArrayList<AttachmentPart> attachments = new ArrayList<AttachmentPart>();
   //
   // Iterator<?> attIterator = responseMessage.getAttachments();
   // while( attIterator.hasNext() ) {
   // AttachmentPart ap = (AttachmentPart)attIterator.next();
   //
   // attachments.add( ap );
   // }
   // return attachments;
   // }

   /**
    * The getDataPartAttached will request a datapart from the server, and have
    * it returned as an attachment. This is (so far) the only method that uses
    * the private doSoapCall method with parseAttachments = true.
    * 
    * @param jobId
    * @param resolution
    * @param horizontalPart
    * @param verticalPart
    * @return
    * @throws SOAPServerException
    * @throws UserRequestException
    */
   public ArrayList<?> getDataPartAttached(
         long jobId,
         int resolution,
         int horizontalPart,
         int verticalPart ) throws SOAPServerException, UserRequestException
   {

      ObjectTuple kv = doSoapCall(
         "getDataPartAttached",
         true,
         120000,
         true,
         new ObjectTuple( "jobId", jobId ),
         new ObjectTuple( "resolution", resolution ),
         new ObjectTuple( "horizontalPart", horizontalPart ),
         new ObjectTuple( "verticalPart", verticalPart ) );

      return (ArrayList<?>) kv.obj2;
   }

   public ArrayList<Session> getStatus()
      throws SOAPServerException,
      XMLParseException,
      UserRequestException
   {

      ObjectTuple kv = doSoapCall(
         "getStatus",
         new ObjectTuple( "hostname", localClientIdentifier ) );

      ArrayList<Session> alteredSessions = null;
      if( kv.obj2 == null )
         return null;

      try
      {
         XStream xstream = new XStream( new DomDriver() );
         xstream.alias( "Session", Session.class );
         xstream.alias( "Job", Job.class );

         alteredSessions = (ArrayList<Session>) xstream
               .fromXML( (String) kv.obj2 );
         // System.out.println("alteredSessions: ["+(String)kv.obj2+"]");
         /*
          * for( Session s : alteredSessions ) { System.out.println( "Session: " +
          * s ); for( Job j : s.alteredJobs ) { System.out.println( "Job: " + j ); } }
          */
         return alteredSessions;
      }
      catch( Exception e )
      {
         throw new XMLParseException(
                                      "Method getStatus failed because of XML parsing error",
                                      e );
      }
   }

   public ArrayList<Parameter> getParametersForSessionWithOutput(
         long sessionId,
         String outputName )
      throws SOAPServerException,
      XMLParseException,
      UserRequestException
   {
      ObjectTuple kv = doSoapCall(
         "getParametersForSession",
         new ObjectTuple( "sessionId", sessionId ),
         new ObjectTuple( "outputName", outputName ) );

      if( kv.obj2 == null )
         return null;

      try
      {
         XStream xstream = new XStream( new DomDriver() );
         xstream.alias( "Parameter", Parameter.class );

         ArrayList<Parameter> parameters = (ArrayList<Parameter>) xstream
               .fromXML( (String) kv.obj2 );
         // for( Parameter p : parameters ) {
         // System.out.println( "Parameter: " + p );
         // }

         return parameters;
      }
      catch( Exception e )
      {
         throw new XMLParseException(
                                      "Method getStatus failed because of XML parsing error:["
                                            + (String) kv.obj2 + "]",
                                      e );
      }
   }

   public ArrayList<ParameterValue> getJobParameters( long jobId )
      throws SOAPServerException,
      XMLParseException,
      UserRequestException
   {
      ObjectTuple kv = doSoapCall(
         "getJobParameters",
         new ObjectTuple( "jobId", jobId ) );

      if( kv.obj2 == null )
         return null;

      try
      {

         XStream xstream = new XStream( new DomDriver() );
         xstream.alias( "ParameterValue", ParameterValue.class );

         xstream.alias( "DataObject", DataObject.class );
         xstream.alias( "Bounds", Bounds.class );
         xstream.alias( "Dimension", Dimension.class );

         ArrayList<ParameterValue> parameters = (ArrayList<ParameterValue>) xstream
               .fromXML( (String) kv.obj2 );
         /*
          * for( ParameterValue p : parameters ) { System.out.println(
          * "ParameterValue: " + p +", "+p.value+", "+p.value.getClass()); }
          */

         return parameters;
      }
      catch( Exception e )
      {
         throw new XMLParseException(
                                      "Method getStatus failed because of XML parsing error",
                                      e );
      }

   }

   public void createUserDefinedLookmark(
         long jobId,
         boolean isPersonal,
         String description ) throws SOAPServerException, UserRequestException
   {

      doSoapCall(
         "createUserDefinedLookmark",
         new ObjectTuple( "jobId", jobId ),
         new ObjectTuple( "isPersonal", isPersonal ),
         new ObjectTuple( "description", description ) );
   }

   private ObjectTuple doSoapCall( String methodName, ObjectTuple... parameters )
      throws SOAPServerException,
      UserRequestException
   {
      return doSoapCall( methodName, true, 120000, parameters );
   }

   /**
    * This method will try to parse the axis faultString to find the exception
    * name and message.
    * 
    * @param axisFaultString
    * @return
    */
   private void parseAndThrowException( String axisFaultString )
      throws SOAPServerException,
      UserRequestException
   {
      int index;
      if( ( index = axisFaultString.indexOf( "xception:" ) ) < 0 )
         return;

      String exceptionName = axisFaultString.substring( 0, index + 8 ).trim();
      String exceptionMessage = axisFaultString.substring( index + 10 ).trim();

      if( exceptionName.equalsIgnoreCase( "java.net.ConnectException" ) )
      {
         lastException = new SOAPServerException( exceptionMessage );
         throw (SOAPServerException) lastException;
      }
      if( exceptionName.endsWith( "UserRequestException" )
            || exceptionName.endsWith( "AuthorizationException" ) )
      {
         lastException = new UserRequestException( exceptionMessage );
         throw (UserRequestException) lastException;
      }
      // all other cases of exceptions:
      lastException = new SOAPServerException( "Unexpected " + exceptionName
            + ": " + exceptionMessage );
      throw (SOAPServerException) lastException;
   }

   private ObjectTuple doSoapCall(
         String methodName,
         boolean showErrorIfNeeded,
         int timeout,
         ObjectTuple... parameters )
      throws SOAPServerException,
      UserRequestException
   {

      return doSoapCall(
         methodName,
         showErrorIfNeeded,
         timeout,
         false,
         parameters );
   }

   private ObjectTuple doSoapCall(
         String methodName,
         boolean showErrorIfNeeded,
         int timeout,
         boolean parseAttachments,
         ObjectTuple... parameters )
      throws SOAPServerException,
      UserRequestException
   {
      try
      {
         lastException = null;
         Call call = createCall( methodName, timeout );

         Object[] values = new Object[ parameters.length ];
         int i = 0;
         for( ObjectTuple k : parameters )
         {
            call.addParameter(
               (String) k.obj1,
               org.apache.axis.Constants.XSD_ANYTYPE,
               ParameterMode.IN );
            values[i++] = k.obj2;
         }

         Object ret = call.invoke( values );
         Message rm = call.getResponseMessage();

         // if we have to parse attachments, return an arraylist with
         // byte-arrays (with the soap result as first
         // element) instead of just the soap function result object
         if( parseAttachments )
         {
            ArrayList<Object> attachmentData = new ArrayList<Object>();
            attachmentData.add( ret );
            Iterator<?> attachments = rm.getAttachments();
            while( attachments.hasNext() )
            {
               InputStream is = (InputStream) ( ( (AttachmentPart) attachments
                     .next() ).getContent() );
               int avail = is.available();
               byte[] b = new byte[ avail ];
               is.read( b, 0, avail );
               attachmentData.add( b );
               b = null;
            }
            return new ObjectTuple( rm, attachmentData );
         }
         else
         {
            // no attachments required, just return the soap result
            return new ObjectTuple( rm, ret );
         }
      }
      catch( Exception e )
      {
         lastException = e;

         if( e instanceof AxisFault )
         {
            AxisFault axisFault = (AxisFault) e;
            parseAndThrowException( axisFault.getFaultString() );
         }
         lastException = new SOAPServerException( "Unexpected exception: "
               + e.getMessage() );

         throw (SOAPServerException) lastException;
      }
   }

   private Call createCall( String methodName, int timeout )
      throws ServiceException
   {
      Call call;
      call = (Call) ( Communication.service   ).createCall();
      call.setTimeout( timeout );
      call.setMaintainSession( true );
      // call.setStreaming(true);
      call.setTargetEndpointAddress( endpoint );
      call.setOperationName( new QName( methodName ) );
      call.setReturnType( org.apache.axis.Constants.XSD_ANY );
      call.clearHeaders();
      call.setUsername( username );
      call.setPassword( password );

      return call;
   }

   public String getServerAddress()
   {
      return serverAddress;
   }

}
